package strain.demo;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import strain.domain.InconsistentDomainException;

public class Sudoku {
    
    // in sudoku, boxes can only hold integers in [1 9], create a domain for these
    private final static Set<Integer> cellDomain = new TreeSet<Integer>();
    static {
        for(int i = 1; i <= 9; ++i) cellDomain.add(i);
    }
    private final List<List<DiscreteVariable<Integer>>> cellRows;
    private final List<List<DiscreteVariable<Integer>>> cellCols;
    private final List<List<DiscreteVariable<Integer>>> cellBoxes;
    
    public Sudoku(){
        this.cellRows = new ArrayList<List<DiscreteVariable<Integer>>>(9);
        this.cellCols = new ArrayList<List<DiscreteVariable<Integer>>>(9);
        this.cellBoxes = new ArrayList<List<DiscreteVariable<Integer>>>(9);
        for(Integer row = 1; row <= 9; ++row){
            List<DiscreteVariable<Integer>> cellRow = new ArrayList<DiscreteVariable<Integer>>(9);
            for(Integer col = 1; col <= 9; ++col){
                cellRow.add(new DiscreteVariable<Integer>(row.toString() + "," + col.toString(), cellDomain));
            }
            this.cellRows.add(cellRow);
        }
        for(int i = 0; i < 9; ++i){
            this.cellCols.add(new ArrayList<DiscreteVariable<Integer>>(9));
            this.cellBoxes.add(new ArrayList<DiscreteVariable<Integer>>(9));
        }
        for(int rowIdx = 0; rowIdx < 9; ++rowIdx){
            for(Integer colIdx = 0; colIdx < 9; ++colIdx){
                int rowOfBoxIdx = rowIdx/3;
                int colOfBoxIdx = colIdx/3;
                int boxIdx = rowOfBoxIdx*3 + colOfBoxIdx;
                DiscreteVariable<Integer> cell = this.cellRows.get(rowIdx).get(colIdx);
                this.cellCols.get(colIdx).add(cell);
                this.cellBoxes.get(boxIdx).add(cell);
            }
        }
    }
    
    public DiscreteVariable<Integer> getCell(final int row, final int col){
        if ((1 > row) || (row > 9) ||(1 > col) || (col > 9))
            throw new IllegalArgumentException("row and col must be between 1 and 3 inclusive");

        int rowIdx = row-1;
        int colIdx = col-1;
        return this.cellRows.get(rowIdx).get(colIdx);
    }
    
    public Integer getValue(final int row, final int col){
        DiscreteVariable<Integer> cell = getCell(row, col);
        if (cell.isBound()) return cell.getBoundValue();
        return null;
    }
    
    public List<DiscreteVariable<Integer>> getRow(final int row){
        if ((1 > row) || (row > 9))
            throw new IllegalArgumentException("row must be between 1 and 9 inclusive, passed in: " + row);
        
        int rowIdx = row-1;
        return this.cellRows.get(rowIdx);
    }
    
    public List<DiscreteVariable<Integer>> getColumn(final int col){
        if ((1 > col) || (col > 9))
            throw new IllegalArgumentException("col must be between 1 and 9 inclusive");
        
        int colIdx = col-1;
        return this.cellCols.get(colIdx);
    }
    
    public List<DiscreteVariable<Integer>> getBox(final int rowOfBox, final int colOfBox){
        if ((1 > rowOfBox) || (rowOfBox > 3) ||(1 > colOfBox) || (colOfBox > 3))
            throw new IllegalArgumentException("rowOfBox and colOfBox must be between 1 and 3 inclusive");
        
        int rowOfBoxIdx = rowOfBox-1;
        int colOfBoxIdx = colOfBox-1;
        int boxIdx = rowOfBoxIdx*3 + colOfBoxIdx;
        return this.cellBoxes.get(boxIdx);
    }
    
    public void set(final int row, final int col, final Integer value) throws InconsistentDomainException{
        if ((1 > row) || (row > 9) ||(1 > col) || (col > 9))
            throw new IllegalArgumentException("row and col must be between 1 and 9 inclusive");
        if (value == null)
            throw new IllegalArgumentException("cannot set null value, use unset()");
        
        int rowIdx = row-1;
        int colIdx = col-1;
        DiscreteVariable<Integer> cell = this.cellRows.get(rowIdx).get(colIdx);
        cell.bind(value);
    }
    
    public void set(final int rowOfBox, final int colOfBox, final int rowInBox, final int colInBox, final Integer value) throws InconsistentDomainException{
        if ((1 > rowOfBox) || (rowOfBox > 3) ||(1 > colOfBox) || (colOfBox > 3) ||
            (1 > rowInBox) || (rowInBox > 3) ||(1 > colInBox) || (colInBox > 3))
            throw new IllegalArgumentException("rowOfBox, colOfBox, rowInBox and colInBox must be between 1 and 3 inclusive");
        
        int row = (rowOfBox-1) * 3 + (rowInBox-1) + 1;
        int col = (colOfBox-1) * 3 + (colInBox-1) + 1;
        set(row, col, value);
    }
    
    public void setRow(final int row, final Integer... values) throws InconsistentDomainException{
        if ((1 > row) || (row > 9))
            throw new IllegalArgumentException("row must be between 1 and 9 inclusive");
        if (values.length != 9)
            throw new IllegalArgumentException("must pass exactly 9 values aside from row as arguments to this method");
        for(int i = 0; i < 9; ++i){
            if ((values[i] != null) && ((1 > values[i]) || (values[i] > 9)))
                throw new IllegalArgumentException("all arguments must be null, or between 1 and 9 inclusive");
        }
        
        for(int colIdx = 0; colIdx < 9; ++colIdx){
            int col = colIdx+1;
            if (values[colIdx] != null) set(row, col, values[colIdx]);
        }
    }
    
    public void setRow(final int row, final String values) throws InconsistentDomainException{
        if ((1 > row) || (row > 9))
            throw new IllegalArgumentException("row must be between 1 and 9 inclusive");
        if (values.length() != 9)
            throw new IllegalArgumentException("String argument must be exactly length() == 9");
        Integer[] arrayValues = new Integer[9];
        for(int i = 0; i < 9; ++i){
            int v = (values.charAt(i) - '0');
            if ((1 <= v) && (v <= 9))
                arrayValues[i] = v;
        }
        setRow(row, arrayValues);
    }
    
    @Override
    public String toString(){
        String borderRowLight = "+---+---+---+---+---+---+---+---+---+\n";
        String borderRowDark = "#===#===#===#===#===#===#===#===#===#\n";
        String bgrndLight = " ";
        String bgrndDark = "_";
        StringBuilder sb = new StringBuilder();

        for(int row = 1; row <= 9; ++row){
            int rowIndex = row-1;
            sb.append((rowIndex%3 == 0) ? borderRowDark : borderRowLight);
            for(int col = 1; col <= 9; ++col){
                int rowOfBox = (row-1)/3;
                int colOfBox = (col-1)/3;
                Integer value = getValue(row, col);
                
                sb.append("|");
                boolean dark = ((rowOfBox+colOfBox)%2 == 1);
                sb.append(dark ? bgrndDark : bgrndLight);
                sb.append((value == null) ? dark ? bgrndDark : bgrndLight : value);
                sb.append(dark ? bgrndDark : bgrndLight);
            }
            sb.append("|\n");
        }
        sb.append(borderRowDark);
        
        return sb.toString();
    }
    
    public boolean solve(){
        Solver<Integer> solver = new Solver<Integer>();
        
        // add variables to solver
        for(int row = 1; row <= 9; ++row){
            for(int col = 1; col <= 9; ++col){
                solver.add(getCell(row, col));
            }
        }
        
        // add constraints requiring that a number cannot appear in a row twice
        // add constraints requiring that a number cannot appear in a column twice
        // add constraints requiring that a number cannot appear in a box twice
        for(int i = 0; i < 9; ++i){
            solver.add(new AllDifferent<Integer>(getRow(i+1)));
            solver.add(new AllDifferent<Integer>(getColumn(i+1)));
            solver.add(new AllDifferent<Integer>(getBox(i/3+1, i%3+1)));
        }
        
        return solver.solve();
    }
    
    public static void main(final String[] args){
        System.out.println("Hello World!");
        System.out.println(new Date());
        
        Sudoku sudoku = new Sudoku();
        try{
            sudoku.setRow(1, "3_8__1___");
            sudoku.setRow(2, "_______7_");
            sudoku.setRow(3, "7__32__1_");
            sudoku.setRow(4, "_4_2__9_5");
            sudoku.setRow(5, "____3____");
            sudoku.setRow(6, "9_6__8_2_");
            sudoku.setRow(7, "_8__13__6");
            sudoku.setRow(8, "_5_______");
            sudoku.setRow(9, "___5__4_8");
            
            // this is the hardest sudoku ever made
//            sudoku.setRow(1, "85___24__");
//            sudoku.setRow(2, "72______9");
//            sudoku.setRow(3, "__4______");
//            sudoku.setRow(4, "___1_7__2");
//            sudoku.setRow(5, "3_5___9__");
//            sudoku.setRow(6, "_4_______");
//            sudoku.setRow(7, "____8__7_");
//            sudoku.setRow(8, "_17______");
//            sudoku.setRow(9, "____36_4_");
        } catch (InconsistentDomainException ide){
            System.out.println("problem setup issue?  this should never happen.");
            throw new RuntimeException(ide);
        }
        
        System.out.println(sudoku);

      if (sudoku.solve()){
          System.out.println("Problem solved!");
          System.out.println(sudoku);
      } else {
          System.out.println("Problem was infeasible.");
      }

        System.out.println(new Date());
        System.out.println("total reductions: " + DiscreteVariable.getReductionCount());
        System.out.println("total useless reduction calls: " + DiscreteVariable.getNonReductionCount());
        System.out.println("total reductions to empty: " + DiscreteVariable.getReducedToEmptyCount());
        
        System.out.println("solver depth (should be zero): " + Solver.getDepth());
        System.out.println("solver max depth: " + Solver.getMaxDepth());
        
        System.out.println("Goodbye World!");
    }
    
}
